// React中的状态提升 (第二个版本，重要)

/*
    在提供摄氏度输入的基础之上，再提供一个华氏温度输入，并且它们能保持同步(两个会同时计算)。
    从 Calculator 组件中抽离一个 TemperatureInput 组件。我们也会给它添加一个值为 c 或 f 的表示温度单位的 scale 属性。
*/

import React, { Component } from 'react';
import './Calculator.css';

function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>水会烧开</p>;
    }
    return <p>水不会烧开</p>;
}

// 华氏度和摄氏度的计算
function toCelsius(fahrenheit) {
    return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
    return (celsius * 9 / 5) + 32;
}

/* 我们还需要另外一个函数，它接受两个参数，第一个接受字符串 temperature 变量，第二个参数则是上面编写的单位转换函数。
   最后会返回一个字符串。我们会使用它来根据一个输入框的输入计算出另一个输入框的值。
   我们最后取到输出的小数点后三位，而 temperature 输入不合法的时候，这个函数则会返回空字符串。
   举两个例子，tryConvert('abc', toCelsius) 会返回空字符串，而 tryConvert('10.22', toFahrenheit) 会返回 '50.396'。
*/
function tryConvert(temperature, convert) {
    // 将输入框中的值转为浮点数
    const input = parseFloat(temperature);
    // 如果不是数字，则返回空字符串
    if (Number.isNaN(input)) {
        return '';
    }
    // 计算华氏度/摄氏度
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return rounded.toString();
}

/*
    状态提升：
        到这一步为止，两个TemperatureInput组件都是在自己的 state 中独立保存数据。

        但是，我们想要的是这两个输入能保持同步。当我们更新摄氏输入（Celsius）时，华氏度（Fahrenheit ）这个框应该能显示转换后的的温度数值，反之亦然。

        在React中，状态分享是通过将state数据提升至离需要这些数据的组件最近的父组件来完成的。这就是所谓的 “状态提升” 。我们会将 TemperatureInput(子组件) 组件自身保存的 state(数据) 移到 Calculator(父组件) 中。

        如果 Calculator 组件拥有了提升上来共享的状态数据，那它就会成为两个温度输入组件的“数据源”。它会传递给下面温度输入组件一致的数据。由于两个 TemperatureInput 温度组件的props属性都是来源于共同的父组件 Calculator，它们的数据也会保持同步。

        让我们一步一步来分析如何操作。

        首先，我们在 TemperatureInput 组件中将 this.state.temperature 替换为 this.props.temperature 。从现在开始，我们假定 this.props.temperature 属性已经存在了，不过之后仍然需要将数据从 Calculator 组件中传进去。
*/

const scaleNames = {
    c: "Celsius 摄氏度",
    f: "Fahrenheit 华氏度"
}

/*
    总结下 TemperatureInput 组件的改变,将其自身的 state 从组件中移除，使用 this.props.temperature 替代 this.state.temperature ，当我们想要响应数据改变时，使用父组件提供的 this.props.onTemperatureChange() 而不是this.setState() 方法：
*/
class TemperatureInput extends Component {
    constructor(props){
        super(props);
        this.handleChange = this.handleChange.bind(this);

        // 我们不需要子组件本身的状态了
        // this.state = {
        //     temperature: '',
        // }

    }
    handleChange(e){
        // 这是之前的代码，TemperatureInput组件本身更改自身的状态，现在调用父组件的prop属性值
        // this.setState({
        //     temperature: e.target.value
        // })

        // 现在我们改成这样：
        this.props.onTemperatureChange(e.target.value);
    }
    render(){
        // 之前的代码是：const temperature = this.state.temperature; 现在我们需要从父组件Calculator2中拿到温度数据。
        const temperature = this.props.temperature;
        // 我们首先知道props是只读的。而之前temperature变量是被保存在其自身的 state 中的，TemperatureInput 组件只需要调用 this.setState() 就能改变它。但现在，temperature 是作为 prop 从父组件传递下来的，TemperatureInput 组件是没有控制权的。
        // 在React中，这个问题通常是通过让组件“受控”来解决。就像 < input > 能够接受 value 和 onChange 这两个prop属性值，自定义组件 TemperatureInput 也能接受来自 Calculator 父组件的 temperature 变量和 onTemperatureChange 方法作为props属性值。
        // 做完这些，当 TemperatureInput 组件更新它的温度数值时，就会调用 this.props.onTemperatureChange 方法。

        const scale = this.props.scale;
        return (
            <fieldset>
                <legend>请输入{scaleNames[scale]}:</legend>
                <input value={temperature}
                    onChange={this.handleChange} 
                />
            </fieldset>
        )
    }
}

/*
    思考一下 Calculator 组件：
    在它的 state 中存储之前输入框组件的 temperature 和 scale 值，这是从输入框组件中“提升”上来的 state，它将会成为两个输入框组件的“数据源”。这是我们所需要的能够重新渲染并且表示两个不同输入组件的最基本的数据。
    举个例子，假如我们在摄氏度输入框中输入37，那么 Calculator 的 state 就是：
        {
            temperature: '37',
            scale: 'c'
        }
    如果我们之后在华氏度输入框输入212，那么 Calculator 的状态数据就会是：
        {
            temperature: '212',
            scale: 'f'
        }

    其实我们可以一起保存两个输入的值，但这么做似乎没有必要。保存最近 改变的值和所需标识的温标单位就足够了。我们可以只需基于当前的 temperature 和 scale 计算出另一个输入框中的值。
    现在这两个输入框中的值能保持同步了，因为它们使用的是通过同一个 state 计算出来的值。
*/

class Calculator2 extends React.Component {
    constructor(props) {
        super(props);
        this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
        this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
        this.state = { 
            temperature: '', 
            scale: 'c' 
        };
    }

    handleCelsiusChange(temperature) {
        this.setState({ scale: 'c', temperature });
    }

    handleFahrenheitChange(temperature) {
        this.setState({ scale: 'f', temperature });
    }

    render() {
        const scale = this.state.scale;
        const temperature = this.state.temperature;
        const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
        const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

        
        // 两个子组件会调用同一个父组件传递的(props)的属性onTemperatureChange，这样让两个数据实现同步。
        return (
            <div>
                (状态提升第二个版本)
                <TemperatureInput
                    scale="c"
                    temperature={celsius}
                    onTemperatureChange={this.handleCelsiusChange} />

                <TemperatureInput
                    scale="f"
                    temperature={fahrenheit}
                    onTemperatureChange={this.handleFahrenheitChange} />

                <BoilingVerdict
                    celsius={parseFloat(celsius)} />

            </div>
        );
    }
}

/*
    现在，无论你编辑哪一个输入框，Calculator 组件中 this.state.temperature 和 this.state.scale 都会更新。其中之一的输入框得到用户原样输入的值，另一个输入框总是显示基于这个值计算出的结果。

    编辑输入框时所发生的一系列活动：

    · React在DOM原生组件<input>上调用指定的onChange函数。在本例中，指的是TemperatureInput组件上的handleChange函数。
    
    · TemperatureInput组件的handleChange函数会在值发生变化时调用this.props.onTemperatureChange()函数。这些props属性，像onTemperatureChange都是由父组件Calculator提供的。
    
    · 当最开始渲染时，Calculator组件把内部的handleCelsiusChange方法指定给摄氏输入组件TemperatureInput的onTemperatureChange方法，并且把handleFahrenheitChange方法指定给华氏输入组件TemperatureInput的onTemperatureChange。两个Calculator内部的方法都会在相应输入框被编辑时被调用。
    
    · 在这些方法内部，Calculator组件会让React使用编辑输入的新值和当前输入框的温标来调用this.setState()方法来重渲染自身。
    
    · React会调用Calculator组件的render方法来识别UI界面的样子。基于当前温度和温标，两个输入框的值会被重新计算。温度转换就是在这里被执行的。
    
    · 接着React会使用Calculator指定的新props来分别调用TemperatureInput组件.React也会识别出子组件的UI界面。
    
    · React DOM 会更新DOM来匹配对应的值。我们编辑的输入框获取新值，而另一个输入框则更新经过转换的温度值。
    
    · 一切更新都是经过同样的步骤，因而输入框能保持同步的。
*/

export default Calculator2;


/*
    经验：

        在React应用中，对应任何可变数据理应只有一个单一“数据源”。通常，状态都是首先添加在需要渲染数据的组件中。此时，如果另一个组件也需要这些数据，你可以将数据提升至离它们最近的父组件中。你应该在应用中保持 自上而下的数据流，而不是尝试在不同组件中同步状态。

        状态提升比双向绑定方式要写更多的“模版代码”，但带来的好处是，你也可以更快地寻找和定位bug的工作。因为哪个组件保有状态数据，也只有它自己能够操作这些数据，发生bug的范围就被大大地减小了。此外，你也可以使用自定义逻辑来拒绝或者更改用户的输入。

        如果某些数据可以由props或者state提供，那么它很有可能不应该在state中出现。举个例子，我们仅仅保存最新的编辑过的temperature和scale值，而不是同时保存 celsiusValue 和 fahrenheitValue 。另一个输入框中的值总是可以在 render() 函数中由这些保存的数据计算出来。这样我们可以根据同一个用户输入精准计算出两个需要使用的数据。
*/